package cn.cai.config;

import cn.cai.handler.MyAccessDeniedHandler;
import cn.cai.handler.MyAuthenticationFailureHandler;
import cn.cai.handler.MyAuthenticationSucessHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;

/**
 * SpringSecurity 验证授权流程的配置类:
 *  可以自定义配置
 */
@Configuration
public class MySpringSecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Bean
    public PasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }


    @Autowired
    private MyAccessDeniedHandler myAccessDeniedHandler;

    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

    @Autowired
    private UserDetailsService userDetailsService;

    /**
     * 重写父类型中的配置逻辑。
     * 如果调用了supper.configure，则使用父类型的默认流程配置。
     * 需要自定义，则删除supper.configure方法的调用
     * 当提供自定义配置，删除super.configure方法的时候，所有的默认流程全部清空。
     *
     * http.formLogin().loginProcessingUrl(String path);
     *      用户登录请求地址。就是处理用户登录逻辑的地址
     *      SpringSecurity提供的处理登录请求控制器，是path监听软编码的。可以通过此方法动态配置监听地址。
     *      只要配置地址和页面的请求地址一致，即可完成登录逻辑。
     *      登录成功: 默认返回的成功页面的“/”。如果有前置请求，则默认返回前置地址。
     *          如：直接访问 loginPage，登录成功进入 ‘/’
     *          如：访问/ok,未登录，进入loginPage，登录成功后，重新访问
     *      登录失败: 默认返回的页面是 http.formLogin().loginPage(String path) 方法参数?error
     *
     * http.formLogin().defaultSuccessUrl(String path, boolean alwaysUse)
     *      设置登录成功后，响应重定向地址。Spring Security 要求必须传递绝对路径。如果传递的是相对路径，
     *      则相当于当前服务器的根，开始寻址。http://localhost:port/
     *      参数alwaysUse，可以省略。默认为false。配置为false的时候，通常
     *
     *  登录请求参数命名：
     *      默认规则是：用户名必须是username。密码必须是paswword。
     *      此逻辑由UsernamePasswordAuthenticationFilter决定。
     *
     * @param http 基于HTTP协议的Security配置对象。包含所有的SpringSecurity相关配置逻辑。
     * @throws Exception  当配置错误的时候，抛出
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 配置登录请求相关内容。
        http.formLogin()
                .loginPage("/toLogin") //当用户未登录的时候，跳转的登录页面地址; 默认是 /login
                .loginProcessingUrl("/login") // 用户登录逻辑请求地址是什么。默认是 /login
                .usernameParameter("username")  // 自定义登录用户名参数名，默认为username
                .passwordParameter("password") // 自定义登录密码参数名，默认为password
                .successHandler(new MyAuthenticationSucessHandler("/toMain", true))    // 使用自定义请求处理控制逻辑（Handler）实现登录成功跳转处理器，GET＼POST都可以
                .failureHandler(new MyAuthenticationFailureHandler("/failure",true)); // 自定义认证失败后的处理逻辑。
                //.failureUrl("/failure");    // 登录失败后，重定向的位置。记得提供权限放行
                //.failureForwardUrl("/failure") //登录失败后，请求转发的位置。Security请求转发使用post请求。默认使用请求转发。
                //.defaultSuccessUrl("/toMain", true); // 用户登录成功后，响应重定向到的位置。GET请求。必须配置绝对地址。

                //.successForwardUrl("/toMain"); // 用户登录成功后，请求转发到的位置。security请求转发使用post


        // 配置权限检验。 如：什么地址必须认证后才能访问，什么地址可以不认证就访问
        // 如：登录页面，不认证就可以访问，其他页面必须认证后才可以访问。
        // 权限校验的配置，是线性的。从开始的配置位置开始检验，成功立刻返回。
        // 校验匹配失败，继续后续的校验逻辑
        // 如：请求 /toLogin 。校验antMatchers成功，授予所有权限，返回。后续的anyRequests不执行。
        // 如：请求 /suibian 。校验antMathchers失败，执行后续的anyRequests校验，要求必须认证后才能访问。
        // 所有的检验失败，都返回到loginPage 登录页面。
        /**
         * Security 的权限管理，是基于匹配路径后，授予权限的顺序规则。先通过表达式匹配当前请求的路径地址
         * 在请求地址符合逻辑的时候，授予访问权限。
         * 常用的匹配请求路径的方法。执行顺序按照定义顺序依次执行。任何方法匹配，返回。后续逻辑不执行。
         * antMatchers  - 程序员最常用的，是基于 * | /** 等匹配符号，定于的表达式
         *  antMatchers(String... path); 任意请求方式，都有效的匹配。参数传入的规则任意一个符号匹配逻辑
         *      都生效。如：antMatchers("/a","/b","/c/*")  请求地址是 :/a  /b  /c/1 /c/ 都符合
         *  antMatchers(HttpMethod method, String... path); 限定了请求方式的匹配规则。
         *
         * regexMatchers - 程序员排斥，但是推荐使用的。相对效率高。基于正则表达式定义的路径匹配逻辑的。
         *  regexMatchers(String... path); //
         *  regexMatchers(HttpMethod method,String... path); //
         * anyRequest - 相当于 /** 的 antMatchers匹配。任意请求地址
         */
        /**
         * 授予权限的方法逻辑
         *  授予权限的方法，都是基于路径地址匹配基础上调用的。
         *    常用方法：
         *      permitAll() - 免登录访问。任意用户任意状态都可以访问。描述通用资源。如网站首页 描述静态资源的。如：css,js,jpg,png
         *      anonymous() - 匿名访问。登录后不可访问。如：登录页面和登录请求。
         *      authenticated() - 登录后可以访问。描述保护资源的。如：个人信息，订单查看，密码修改等
         *      denyAll()  - 任意用户任意状态都不可访问。屏蔽
         *      fullyAuthenticated() - 完整登录才可以访问。描述敏感资源的。如：钱的操作。
         *      rememberMe() - 记住我，自定登录|n天免登录。非敏感资源的。如：除钱相关操作以外的内容。
         *   上述的所有方法，底层调用的都是统一的一个方法，access()。
         *      access - 可以实现任何权限授予逻辑。是基于权限表达式的权限授予方法。
         *                 表达式规则由Spring Security定义。
         *      表达式种类：
         *          1、定值表达式：access("permitAll") == permitsAll()
         *                 包含：permitAll anonymous authenticated fullAuthenticated
         *                      denyAll rememberMe
         *          2、动态表达式
         */
        http.authorizeRequests()
                .antMatchers("/toLogin").anonymous() //只能匿名访问
                .antMatchers("/failure","/favicon.ico").permitAll() // toLogin 请求地址，可以随便访问。
                .antMatchers("/**/*.js").permitAll() // 授予所有目录下的所有.js文件可随意访问
                .regexMatchers(".*[.]css").permitAll() // 授予所有目录下的所有.css文件可访问

                // 在access方法的参数中，可以使用SpringEL表达式传递代码运行逻辑。
                // @beanName 可以获取Spring容器中的对象。

                // @beanName.methodName()可以调用bean对象中的方法
                // 方法参数要传递的对象，可以在WebSecurityExpressionRoot中查看，是这个类型中的所有属性。
                // .antMatchers("/**").access("@myAuthorityPermitImpl.hasAuthority(request,authentication)")
                //.antMatchers("/ip").access("hasIpAddress('localhost')") // 使用access方式处理客户端ip
                // 根据客户端请求的ip地址进行授权
                // 注意：ip规则访问要求完整匹配。localhost -> 0:0:0:0:0:0:1  127.0.0.1 -> 127.0.0.1  192.168.
                // 客户端地址，可以在请求头中查看到。解析是由 request.getRemoteAddr处理的。
                // 限制客户端的使用。如：提供给内部指定客户端的服务。  使用固定IP比较稳定。
//                .antMatchers("/ip").hasIpAddress("127.0.0.1")  //客户端ip符合规则

                // 基于资源的权限管理
                // hasAuthority("xxx") 等同于 access("hasAuthority('xxx')")
                //.antMatchers("/admin/read").hasAuthority("admin:read") // 拥有admin:read 权限的用户可以访问
                //.antMatchers("/admin/write").access("hasAnyAuthority('admin:write')")
                // access("hasAnyAuthority('guest:read', 'admin:read')") 等同于 hasAnyAuthority('guest:read', 'admin:read')
                //.antMatchers("/guest/read").access("hasAnyAuthority('guest:read', 'admin:read')")
                //.antMatchers("/guest/write").hasAnyAuthority("guest:write")
                // 基于角色的权限管理
                // hasRole(xxx) 相当于调用 access("hasRole('ROLE_xxx')")
                //.antMatchers("/admin/read").hasRole("管理员")  // 请求地址为admin/read的请求，必须登录用户拥有'管理员'角色才可以访问
                // hasAnyRole("x", "y", "z")  相当于access("hasAnyRole('ROLE_x', 'ROLE_y', 'ROLE_z'")
                //.antMatchers("/guest/read").hasAnyRole("管理员","访客")  // 登录用户有访客或管理员角色，可以访问。
                //.antMatchers("/guest/write").hasRole("访客")  // 必须访客才可访问
                .anyRequest().authenticated(); // 任意的请求，都必须认证后才能访问。
        // 配置
        http.exceptionHandling()
                .accessDeniedHandler(myAccessDeniedHandler);  //配置403访问错误处理器.

        //配置rememberMe   利用Cookies实现
        http.rememberMe()
                .rememberMeParameter("remember-me")  //修改请求参数名。默认是remember-me
                .tokenValiditySeconds(30*60) //设置记住我有效时间。单位是秒。默认是7天
                .rememberMeCookieName("remember-me")  //设置remember-me在客户端的Cookies的名字
                .tokenRepository(persistentTokenRepository)  //配置用户登录标记的持久化工具对象。
                        .userDetailsService(userDetailsService); //配置自定义的UserDetailsService接口实现类对象
        // 关闭CSRF安全协议。
        // 关闭是为了保证完整流程的可用。
        http.csrf().disable();
    }

    /**
     * 创建一个持久化用户登录标记的Repository对象。
     * JdbcTokenRepositoryImpl - 对象，是访问数据库的Repository。需要提供数据库链接
     * 所有被@Bean注解描述的方法，其参数都自动从Spring容器中获取对象。
     * 如果没有对象，抛出异常，如果对象不唯一，抛出异常。
     *
     * Security持久化用户登录标记，使用的表格，由Security框架创建。
     * 第一次使用的时候，没有表格，需要让框架创建。后续使用的时候，表格存在，不能让框架创建表格
     * 否则抛出数据库访问异常
     * @return
     */
    @Bean
    public PersistentTokenRepository persistentTokenRepository(DataSource dataSource){
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        // 添加数据源
        jdbcTokenRepository.setDataSource(dataSource);

        // 设置启动的时候，创建表。只有数据库中不存在表格的时候可以使用，默认值为false
        jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }
}
